做數據分析時,我們可能會很常使用jupyter notebook來操作
並且可能依照個人喜好來使用像是seaborn、matplotlib等工具把資料視覺化
但是如果是要呈現在網頁上的話,可能就要找JavaScript可視化庫會更有彈性
因此我選擇ECharts來作為這次的工具
https://echarts.apache.org/en/index.html
Apache ECharts的前身是百度的Echarts,在經過Apache Incubator孵化完成後變成Apache軟體基金會的頂級專案。ECharts是一個使用JavaScript實現的視覺化圖表庫,可以在PC與其他裝置上使用,且具有以下特點
豐富的圖表類型:
https://echarts.apache.org/examples/zh/index.html
除了一般常見的折線圖、柱狀圖、圓餅圖、散佈圖等等,還有包含k線圖、地理座標圖等等,並且也有許多酷炫的動畫呈現
方便:
Echarts內置的dataset屬性支持直接傳入array、key-value等多種格式的數據類型,省去很多時候數據還需要轉換的步驟
主題設計系統:
在示例中找到喜歡的圖點進去,就可以直接看每個圖應該要怎麼生成,並且也可以透過程式碼編輯馬上看到更改後的成果,非常強大與方便
我這邊就不特別介紹Django跟pandas的用法,今天要寫的語法都是很基礎的,所以也會直接掉過架設環境的部分
網路上有很多資源馬上找就有了。另外js的部分因為我對於js了解還很淺,覺得污染眼睛的話感到抱歉XDD

架構圖如上~
HTML:
Javascript:
Django:
以上就是大致的流程,其實每個圖都大同小異,只要會了其中一種剩下的也不會到太困難
動態的那些圖或是需要第三方計算回歸線那些,我不確定做起來後用pagespeed分析後會不會分數很慘XD
所以我自己在使用上應該還是簡單為主,另外我發現如果把幾種基本圖的code全部放上來,篇幅有點太長了
所以這次就先以折線圖來說~

這是官方示例的圖
接下來我們可以想一下,哪些部分是需要用到我們自己資料的
HTML:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>測試echart</title>
    <script src="https://cdn.bootcss.com/jquery/3.0.0/jquery.min.js"></script>
    <!--引入ECharts CDN-->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/echarts/5.4.3/echarts.min.js"></script>
</head>
<body>
<!--設置DOM元素 並設置大小-->
<div id="ecahrtLine" style="width: 100%; height:50vh;"></div>
<!--js檔-->
<script src="/static/js/echart_theme/line.js"></script>
</body>
</html>
這邊就是引入CDN跟設置元素,我自己是還有再額外引入jqery,因為我在js有用到相關語法
Django-views.py:
from random import randrange
import pandas as pd
def create_line_data():
    math_score = {
        "math_score": [randrange(50, 90) for _ in range(6)]
    }
    english_score = {
        "english_score": [randrange(60, 100) for _ in range(6)]
    }
    m_series = pd.Series(math_score)
    e_series = pd.Series(english_score)
    return m_series, e_series
def trans_df_to_list(series):
    """因為echart的data需要接收list 所以需要轉格式"""
    if isinstance(series, pd.Series):
        return series.values[0]
    else:
        return
def create_data(*args):
    res = [i for i in args]
    return res
def show_line(request):
    m_series, e_series = create_line_data()
    m_list = trans_df_to_list(m_series)
    e_list = trans_df_to_list(e_series)
    data = {
        "code": 200,
        "msg": "success",
        "data": create_data(m_list, e_list),
    }
    return JsonResponse(data)
首先我這邊想要呈現的是兩個科目中,這一個班級的6位學生他們各自的分數
這邊可能要注意幾個點:
Javascript:
var lineDom = document.getElementById('ecahrtLine');
var myLine = echarts.init(lineDom);
$(
    function () {
        fetchData(myLine);
    }
);
function fetchData() {
    $.ajax({
        url: "/article/show_line",
        type: "GET",
        dataType: "json",
        success: function (result) {
            var option = createOption(result.data);
            myLine.setOption(option);
        }
    });
};
function createOption (backendData) {
    // 做出x軸的列表
    var indexList = backendData[0].map(function(_, index) {
        return index + 1;
    });
    var option;
    option = {
          title: {
            text: '數學與英文成績'
          },
          tooltip: {
            trigger: 'axis'
          },
          legend: {},
          toolbox: {
            show: true,
            feature: {
              dataZoom: {
                yAxisIndex: "none"
              },
              dataView: { readOnly: false },
              magicType: { type: ['line', 'bar'] },
              restore: {},
              saveAsImage: {}
            }
          },
          xAxis: {
            type: 'category',
            boundaryGap: false,
            data: indexList,
            name: "學生編號" // 設置名稱
          },
          yAxis: {
            type: 'value',
            min: "dataMin", // 設置y軸最小值
            axisLabel: {
              formatter: '{value} 分'
            },
            name: "成績" // 設置名稱
          },
          series: [
            {
              name: '數學成績',
              type: 'line',
              data: backendData[0], // 我們的資料
              markPoint: {
                data: [
                  { type: 'max', name: 'Max' },
                  { type: 'min', name: 'Min' }
                ]
              },
              markLine: {
                data: [{ type: 'average', name: 'Avg' }]
              }
            },
            {
              name: '英文成績',
              type: 'line',
              data: backendData[1], // 我們的資料
              markPoint: {
                data: [
                  { type: 'max', name: 'Max' },
                  { type: 'min', name: 'Min' }
                  ]
              },
              markLine: {
                data: [
                  { type: 'average', name: 'Avg' },
                  [
                    {
                      symbol: 'none',
                      x: '90%',
                      yAxis: 'max'
                    },
                    {
                      symbol: 'circle',
                      label: {
                        position: 'start',
                        formatter: 'Max'
                      },
                      type: 'max',
                      name: '最高點'
                    }
                  ]
                ]
              }
            }
          ]
        };
    return option
}
因為我沒有特別需要轉換太多x軸的形式,所以我直接用索引來改編我的x軸,今天如果是x軸的資料格式比較特別,或是點超級多,建議在django那邊解決掉
其中在設置一些參數的API,我自己有改的部分有加上註解
如果還是看不懂,可以參考:
https://echarts.apache.org/zh/option.html#xAxis.name
這個官方文檔已經算是非常詳細的解釋API了,並且可以點“試一試”,再點code的部分便可以直接去操作測試
最後的成果如下:
甚至你可以點擊右上方的一些按鈕,會有很多不錯的特效
例如轉換成柱狀圖等等
但是這樣還不夠~
我們此時去更改視窗寬度,會發現圖表根本就沒有變化
而echarts有resize方法,可以讓圖表隨著視窗改變而改變
https://echarts.apache.org/zh/api.html#echartsInstance.resize
那一般我們沒有特別要求的話可以直接這樣調用
// 視窗調整時會更改echart圖表
window.onresize = function () {
    	myLine.resize()
};
這邊額外說一下,如果我們使用下圖這種圓餅圖
我們縮小的時候,會更希望由左右兩邊變成上下並行
echarts還有類似css中media的設置方法,可以參考官方文檔:
https://echarts.apache.org/zh/tutorial.html#%E7%A7%BB%E5%8A%A8%E7%AB%AF%E8%87%AA%E9%80%82%E5%BA%94
好~ 我們回到我們的折線圖,我們的確可以讓圖片的寬度自適應,但是文字不會因為圖片變小而讓佔比放大
這樣對於手機或是平板的使用者會非常痛苦
所以我們需要修改原本的方法,讓font-size也能夠自適應
參考:https://blog.csdn.net/jingjing217/article/details/114015832
原本的js邏輯順序如下
但是現在要修改成:
修改後的js
var lineDom = document.getElementById('ecahrtLine');
var myLine = echarts.init(lineDom);
var rowData;
$(function () {
        fetchData(myLine);
    }
);
// 因為文字不會更改 所以要自己寫方法
function fontMedia(fontSizePx){
    var deviceWidth = window.innerWidth||document.documentElement.clientWidth||document.body.clientWidth;
    if (!deviceWidth) return;
    var fontSize = 150 * (deviceWidth / 1920);
    return fontSizePx*fontSize;
}
function fetchData() {
    $.ajax({
        url: "/article/show_line",
        type: "GET",
        dataType: "json",
        success: function (result) {
            rowData = result.data;
            var option = createOption(result.data);
            myLine.setOption(option);
        }
    });
};
function createOption (backendData) {
    // 做出x軸的列表
    var indexList = backendData[0].map(function(_, index) {
        return index + 1;
    });
    var option;
    option = {
          title: {
            text: '數學與英文成績',
            textStyle: {
                fontSize: fontMedia(0.4) // 讓標題可以隨之改變
            }
          },
          tooltip: {
            trigger: 'axis'
          },
          legend: {},
          toolbox: {
            show: true,
            feature: {
              dataZoom: {
                yAxisIndex: "none"
              },
              dataView: { readOnly: false },
              magicType: { type: ['line', 'bar'] },
              restore: {},
              saveAsImage: {}
            }
          },
          xAxis: {
            type: 'category',
            boundaryGap: false,
            data: indexList,
            name: "學生編號" // 設置名稱
          },
          yAxis: {
            type: 'value',
            min: "dataMin", // 設置y軸最小值
            axisLabel: {
              formatter: '{value} 分'
            },
            name: "成績" // 設置名稱
          },
          series: [
            {
              name: '數學成績',
              type: 'line',
              data: backendData[0],
              markPoint: {
                data: [
                  { type: 'max', name: 'Max' },
                  { type: 'min', name: 'Min' }
                ]
              },
              markLine: {
                data: [{ type: 'average', name: 'Avg' }]
              }
            },
            {
              name: '英文成績',
              type: 'line',
              data: backendData[1],
              markPoint: {
                data: [
                  { type: 'max', name: 'Max' },
                  { type: 'min', name: 'Min' }
                  ]
              },
              markLine: {
                data: [
                  { type: 'average', name: 'Avg' },
                  [
                    {
                      symbol: 'none',
                      x: '90%',
                      yAxis: 'max'
                    },
                    {
                      symbol: 'circle',
                      label: {
                        position: 'start',
                        formatter: 'Max'
                      },
                      type: 'max',
                      name: '最高點'
                    }
                  ]
                ]
              }
            }
          ]
        };
    return option
}
// 視窗調整時會更改echart圖表
window.onresize = function () {
    	var option = createOption(rowData);
        myLine.setOption(option);
        myLine.resize();
};
最後重整後,就可以發現我們的標題可以隨著視窗寬度變化而改變大小


有點懶得做gif所以直接丟圖XD
其他調整font-size就大同小異,就不示範了~
希望有幫助到想做圖的人~